package com.dny.asmtop.op;

import com.dny.asmtop.ASMMethodUtils;
import com.dny.asmtop.Command;
import com.dny.asmtop.MethodContext;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.org.objectweb.asm.commons.GeneratorAdapter;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static com.dny.asmtop.ASMMethodUtils.*;
import static java.lang.String.format;
import static jdk.internal.org.objectweb.asm.Type.getType;

/**
 * Created by jlutt on 2018-01-15.
 * <br>
 * 调用其他对象的方法
 *
 * @author jlutt
 */
public class CommandCall implements Command {

  /**
   * 调用方
   */
  private final Command owner;

  /**
   * 调用方法名
   */
  private final String methodName;

  /**
   * 方法参数
   */
  private final List<Command> arguments = new ArrayList<>();

  public CommandCall(Command owner, String methodName, Command... arguments) {
    this.owner = owner;
    this.methodName = methodName;
    this.arguments.addAll(Arrays.asList(arguments));
  }

  @Override
  public Type type(MethodContext context) {
    List<Class<?>> argumentClasses = new ArrayList<>();
    List<Type> argumentTypes = new ArrayList<>();
    for (Command argument : arguments) {
      argumentTypes.add(argument.type(context));
      if (argument.type(context).equals(getType(Object[].class))) {
        argumentClasses.add(Object[].class);
      } else {
        argumentClasses.add(getJavaType(context.getClassLoader(), argument.type(context)));
      }
    }
    Type returnType;
    try {
      if (context.getThisType().equals(owner.type(context))) {
        for (jdk.internal.org.objectweb.asm.commons.Method method : context.getMethods().keySet()) {
          if (method.getName().equals(methodName)) {
            if (method.getArgumentTypes().length == arguments.size()) {
              Type[] methodTypes = method.getArgumentTypes();
              boolean isSame = true;
              for (int i = 0; i < arguments.size(); i++) {
                if (!methodTypes[i].equals(argumentTypes.get(i))) {
                  isSame = false;
                  break;
                }
              }
              if (isSame) {
                return method.getReturnType();
              }
            }
          }
        }
        throw new NoSuchMethodException();
      } else {
        Class<?> ownerJavaType = getJavaType(context.getClassLoader(), owner.type(context));
        Method method = ownerJavaType.getMethod(methodName, argumentClasses.toArray(new Class<?>[]{}));
        Class<?> returnClass = method.getReturnType();
        returnType = getType(returnClass);
      }

    } catch (NoSuchMethodException e) {
      throw new RuntimeException(String.format("No method %s.%s(%s). %s",
          owner.type(context).getClassName(),
          methodName,
          (!argumentClasses.isEmpty() ? argsToString(argumentClasses) : ""),
          exceptionInGeneratedClass(context)
      ));
    }

    return returnType;
  }

  @Override
  public Type generator(MethodContext context) {
    GeneratorAdapter g = context.getGeneratorAdapter();

    //压入被调用对象参数
    owner.generator(context);

    //压入参数
    List<Class<?>> argumentClasses = new ArrayList<>();
    List<Type> argumentTypes = new ArrayList<>();
    for (Command argument : arguments) {
      argument.generator(context);
      argumentTypes.add(argument.type(context));
      argumentClasses.add(getJavaType(context.getClassLoader(), argument.type(context)));
    }

    Type returnType;
    try {
      if (context.getThisType().equals(owner.type(context))) {
        //调用本对象方法
        jdk.internal.org.objectweb.asm.commons.Method method = null;
        for (jdk.internal.org.objectweb.asm.commons.Method m : context.getMethods().keySet()) {
          if (m.getName().equals(methodName)) {
            if (m.getArgumentTypes().length == arguments.size()) {
              Type[] methodTypes = m.getArgumentTypes();
              boolean isSame = true;
              for (int i = 0; i < arguments.size(); i++) {
                if (!methodTypes[i].equals(argumentTypes.get(i))) {
                  isSame = false;
                  break;
                }
              }
              if (isSame) {
                method = m;
                break;
              }
            }
          }
        }
        if (method == null) throw new NoSuchMethodException();
        g.invokeVirtual(owner.type(context), method);
        return method.getReturnType();
      }
      Class<?> ownerJavaType = getJavaType(context.getClassLoader(), owner.type(context));
      Method method = ownerJavaType.getMethod(methodName, argumentClasses.toArray(new Class<?>[]{}));
      Class<?> returnClass = method.getReturnType();
      returnType = getType(returnClass);

      //调用方法
      invokeVirtualOrInterface(g, ownerJavaType, new jdk.internal.org.objectweb.asm.commons.Method(methodName,
          returnType, argumentTypes.toArray(new Type[]{})));

    } catch (NoSuchMethodException e) {
      throw new RuntimeException(format("No method %s.%s(%s). %s",
          owner.type(context).getClassName(),
          methodName,
          (!argumentClasses.isEmpty() ? argsToString(argumentClasses) : ""),
          exceptionInGeneratedClass(context)));
    }
    return returnType;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    CommandCall that = (CommandCall) o;

    if (arguments != null ? !arguments.equals(that.arguments) : that.arguments != null) return false;
    if (methodName != null ? !methodName.equals(that.methodName) : that.methodName != null) return false;
    if (owner != null ? !owner.equals(that.owner) : that.owner != null) return false;

    return true;
  }

  @Override
  public int hashCode() {
    int result = owner != null ? owner.hashCode() : 0;
    result = 31 * result + (methodName != null ? methodName.hashCode() : 0);
    result = 31 * result + (arguments != null ? arguments.hashCode() : 0);
    return result;
  }
}
